package br.com.vti.leilao.controller;

import br.com.vti.leilao.model.business.Business;
import br.com.vti.leilao.model.business.LanceBusiness;
import br.com.vti.leilao.model.business.exception.ApplicationException;
import br.com.vti.leilao.model.dao.Dao;
import br.com.vti.leilao.model.entity.Ganhador;
import br.com.vti.leilao.model.entity.Lance;
import br.com.vti.leilao.model.entity.Leilao;
import br.com.vti.leilao.model.entity.Usuario;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.WsOutbound;
import org.apache.log4j.Logger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;

/**
 *
 * @author ednardomatos
 */
public class Conexao extends MessageInbound {

    public transient static final Logger logger = Logger.getLogger(Conexao.class);
    
    private Leilao leilao;
    private Usuario usuario;
    private Scheduler sched;
    private final Set<Conexao> conexoes;

    public Conexao(Set<Conexao> conexoes) {
        this.conexoes = conexoes;
    }

    @SuppressWarnings("unchecked")
    public Conexao(Set<Conexao> conexoes, Usuario usuario, Integer idLeilao) {
        this.usuario = usuario;
        this.conexoes = conexoes;
        this.leilao = getLeilao(idLeilao);
        SchedulerFactory sf = new StdSchedulerFactory();
        try {
            sched = sf.getScheduler();
        } catch (SchedulerException ex) {
            logger.error(ex);
        }
    }

    public Leilao getLeilao() {
        return leilao;
    }

    public void setLeilao(Leilao leilao) {
        this.leilao = leilao;
    }

    public Usuario getUsuario() {
        return usuario;
    }

    public void setUsuario(Usuario usuario) {
        this.usuario = usuario;
    }

    public Set<Conexao> getConexoes() {
        return conexoes;
    }

    @Override
    protected void onOpen(WsOutbound outbound) {
        getConexoes().add(this);
        String message = String.format("\"%s\" se conectou no leilão", getUsuario().getNome());
        if (getLeilao() != null && getLeilao().getId() != null) {
            message += " " + getLeilao().getId();
        }
        logger.info(message);
        Lance lance = consultarUltimoLance(getLeilao());
        String retorno = "NAO_ENCONTRADO$Leilão não encontrado!";
        if (getLeilao() != null) {
            Date dataAtual = new Date();
            if (dataAtual.after(getLeilao().getDataInicio())) {
                retorno = "COMECOU$" + getLeilao().getProdutoFk().getNome() + "$" + getLeilao().getTempoDuracao() + "$O leilão irá iniciar após o 1º lance$" + formatarValor(getLeilao().getVlrLanceMin());
                if (lance != null && lance.getId() != null && lance.getId() > 0) {
                    Integer tempo = qtdTempoRestante(new Date(), lance.getHoraLance(), getLeilao().getTempoDuracao());
                    if (tempo > 0) {
                        retorno = criarTextoLance(lance);
                    } else {
                        retorno = "ENCERRADO$" + getLeilao().getProdutoFk().getNome() + "$Este leilão está encerrado$" + lance.getUsuarioFk().getNome() + "$" + formatarValor(lance.getVlrLance());
                        if (lance.getUsuarioFk().getId().equals(getUsuario().getId())) {
                            enviarMsgUsuarioAtual(avaliarCompraIngressoExtra(null, lance), getLeilao().getId());
                        }
                    }
                }
            } else {
                retorno = "VAI_COMECAR$" + getLeilao().getProdutoFk().getNome() + "$Leilão vai começar às " + formatarData(getLeilao().getDataInicio());
            }
        }
        enviarMsgUsuarioAtual(retorno, getLeilao().getId());
    }

    @Override
    protected void onClose(int status) {
        getConexoes().remove(this);
        String message = String.format("\"%s\" se desconectou no leilão", getUsuario().getNome());
        if (getLeilao() != null && getLeilao().getId() != null) {
            message += " " + getLeilao().getId();
        }
        logger.info(message);
        super.onClose(status);
    }
    
    @Override
    protected void onBinaryMessage(ByteBuffer bb) throws IOException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    protected void onTextMessage(CharBuffer cb) throws IOException {
        String msg = cb.toString();
        String retorno;
        Date dataAtual = new Date();
        if (dataAtual.after(getLeilao().getDataInicio())) {
            Lance lance = consultarUltimoLance(getLeilao());
            if (lance != null) {
                Integer tempo = qtdTempoRestante(new Date(), lance.getHoraLance(), getLeilao().getTempoDuracao());
                if (tempo > 0) {
                    retorno = avaliarLance(msg, lance);
                } else {
                    retorno = "ENCERRADO$" + getLeilao().getProdutoFk().getNome() + "$Este leilão está encerrado$" + lance.getUsuarioFk().getNome() + "$" + formatarValor(lance.getVlrLance());
                    if (lance.getUsuarioFk().getId().equals(getUsuario().getId())) {
                        enviarMsgUsuarioAtual(avaliarCompraIngressoExtra(msg, lance), getLeilao().getId());
                    }
                }
            } else {
                retorno = avaliarLance(msg, lance);
            }
        } else {
            retorno = "VAI_COMECAR$" + getLeilao().getProdutoFk().getNome() + "$Leilão vai começar às " + formatarData(getLeilao().getDataInicio());
        }
        if (retorno.contains("JA_GANHOU")) {
            enviarMsgUsuarioAtual(retorno, getLeilao().getId());
        } else {
            broadcast(retorno, getLeilao().getId());
        }
        if (msg.contains("ATUALIZAR_GANHADOR")) {
            criarLanceJob();
        }
    }

    @SuppressWarnings("unchecked")
    private String avaliarCompraIngressoExtra(String msg, Lance lance) {
        String retorno = "COMPRAR_EXTRA";
        Date dataLimite = dataLimiteExtra(lance.getHoraLance());
        if (dataLimite.after(new Date())) {
            if (msg != null && msg.contains("COMPRAR_EXTRA")) {
                WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext();
                Business<Ganhador, ApplicationException> ganhadorBusiness = (Business<Ganhador, ApplicationException>) ctx.getBean("ganhadorBusiness");
                Ganhador ganhador = new Ganhador(null, lance);
                try {
                    List<Ganhador> lista = ganhadorBusiness.listarPorExemplo(ganhador, 1);
                    if (lista != null && !lista.isEmpty()) {
                        ganhador = lista.get(0);
                        if (ganhador.getQtdProduto() < getLeilao().getQtdMaxProduto()) {
                            ganhador.setQtdProduto(ganhador.getQtdProduto() + 1);
                            try {
                                ganhadorBusiness.atualizar(ganhador);
                                retorno += "$Ingresso extra comprado com sucesso";
                            } catch (Exception ex) {
                                logger.error(ex);
                            }
                        }
                    }
                } catch (ApplicationException e) {
                    if (getLeilao().getQtdMaxProduto() >= 2) {
                        try {
                            ganhador.setQtdProduto(2);
                            ganhadorBusiness.inserir(ganhador);
                            retorno += "$Ingresso extra comprado com sucesso";
                        } catch (Exception ex) {
                            logger.error(ex);
                        }
                    }
                }
            } else {
                retorno += "$Atenção ganhador você até " + formatarData(dataLimite) + " para comprar um ingresso extra.";
            }
        } else {
            retorno = "TEMPO_ESGOTADO$Seu tempo para compra de um ingresso extra expirou.";
        }
        return retorno;
    }

    private Date dataLimiteExtra(Date ultLance) {
        Date dataLimite = null;
        if (ultLance != null) {
            GregorianCalendar gc = new GregorianCalendar();
            gc.setTime(ultLance);
            gc.add(GregorianCalendar.MINUTE, getLeilao().getTempoMaxSelectQtd());
            dataLimite = gc.getTime();
        }
        return dataLimite;
    }

    private String avaliarLance(String msg, Lance lance) throws NumberFormatException {
        String retorno = "";
        if (msg != null && msg.contains("Oferecer")) {
            String vlrOfer = msg.replace("Oferecer R$ ", "").replace(",00", "");
            Double doubleVlr = new Double(vlrOfer);
            if (!ganhouLeilao()) {
                if (lance == null || (doubleVlr > lance.getVlrLance() &&
                        qtdTempoRestante(new Date(), lance.getHoraLance(), 2) == 0)) {
                    retorno = oferecerLance(doubleVlr);
                } else {
                    retorno = criarTextoLance(lance);
                }
            } else {
                retorno = "JA_GANHOU$Você já ganhou um leilão";
            }
        }
        return retorno;
    }

    @SuppressWarnings("unchecked")
    private boolean ganhouLeilao() {
        boolean retorno = false;
        WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext();
        Business<Ganhador, ApplicationException> ganhadorBusiness = (Business<Ganhador, ApplicationException>) ctx.getBean("ganhadorBusiness");
        try {
            String hql = "FROM Ganhador g INNER JOIN g.lanceFk.leilaoFk.produtoFk as p WHERE p.id= :idProduto and g.lanceFk.usuarioFk.id = :idUsuario";
            Map<String, Object> parametros = new HashMap<String, Object>();
            parametros.put("idProduto", getLeilao().getProdutoFk().getId());
            parametros.put("idUsuario", getUsuario().getId());
            List<Ganhador> lista = ganhadorBusiness.getDao().listarPorHql(hql, parametros, 10);
            retorno = (lista != null && !lista.isEmpty());
        } catch (SQLException ex) {
            logger.error(ex.getMessage());
        }
        return retorno;
    }

    private String formatarData(Date data) {
        String strData = null;
        if (data != null) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
            strData = dateFormat.format(data);
        }
        return strData;
    }

    @SuppressWarnings("unchecked")
    private String oferecerLance(Double doubleVlr) {
        String retorno = null;
        Lance lance = new Lance(doubleVlr, getUsuario(), getLeilao());
        WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext();
        Business<Lance, ApplicationException> lanceBusiness = (Business<Lance, ApplicationException>) ctx.getBean("lanceBusiness");
        try {
            lanceBusiness.inserir(lance);
            lance = lanceBusiness.procurarPorId(lance);
            if (lance != null && lance.getId() != null && lance.getId() > 0) {
                retorno = criarTextoLance(lance);
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
        }
        return retorno;
    }

    private String criarTextoLance(Lance lance) {
        StringBuilder retorno = null;
        if (lance != null && lance.getId() != null && lance.getId() > 0 && getLeilao() != null) {
            retorno = new StringBuilder("LANCE$");
            retorno.append(qtdTempoRestante(new Date(), lance.getHoraLance(), getLeilao().getTempoDuracao()));
            retorno.append("$");
            retorno.append(getLeilao().getProdutoFk().getNome());
            retorno.append("$");
            String nome = lance.getUsuarioFk().getNome();
            if (nome.length() > 22) {
                nome = nome.substring(0, 22);
            }
            retorno.append(nome);
            retorno.append("$");
            retorno.append(formatarValor(lance.getVlrLance()));
            retorno.append("$");
            retorno.append(formatarValor(lance.getVlrLance() + getLeilao().getVlrIncLance()));
            retorno.append("$");
            retorno.append(getLeilao().getTempoDuracao());
        }
        return retorno.toString();
    }

    private String formatarValor(Double valor) {
        return valor.toString().replace(".0", "");
    }

    @SuppressWarnings("unchecked")
    private Lance consultarUltimoLance(Leilao leilao) {
        Lance lance = new Lance();
        lance.setLeilaoFk(leilao);
        WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext();
        LanceBusiness lanceBusiness = (LanceBusiness) ctx.getBean("lanceBusiness");
        return lanceBusiness.consultarUltimoLance(lance);
    }
    
    @SuppressWarnings("unchecked")
    private Leilao getLeilao(Integer idLeilao) {
        Leilao leilaoAux = null;
        try {
            leilaoAux = new Leilao();
            leilaoAux.setId(idLeilao);
            WebApplicationContext ctx = ContextLoader.getCurrentWebApplicationContext();
            Dao<Leilao> leilaoDao = (Dao<Leilao>) ctx.getBean("dao");
            leilaoAux = leilaoDao.procurarPorId(leilaoAux);
        } catch (SQLException ex) {
            logger.error(ex);
        }
        return leilaoAux;
    }
    
    public void broadcast(String msg, Integer id) {
        if (msg != null) {
            for (Conexao conexao : getConexoes()) {
                if (conexao.getLeilao() != null && conexao.getLeilao().getId().equals(id)) {
                    try {
                        CharBuffer buffer = CharBuffer.wrap(msg);
                        conexao.getWsOutbound().writeTextMessage(buffer);
                        logger.info(msg);
                    } catch (IOException ex) {
                        logger.error(ex);
                    }
                }
            }
        }
    }
    
    private void enviarMsgUsuarioAtual(String msg, Integer id) {
        if (msg != null) {
            try {
                CharBuffer buffer = CharBuffer.wrap(msg);
                getWsOutbound().writeTextMessage(buffer);
                logger.info(msg);
            } catch (IOException ex) {
                logger.error(ex);
            }
        }
    }

    private Integer qtdTempoRestante(Date dataAtual, Date dataUltimoLance, Integer tempoDuracao) {
        Integer res = 0;
        GregorianCalendar gc1 = new GregorianCalendar();
        gc1.setTime(dataAtual);
        
        GregorianCalendar gc2 = new GregorianCalendar();
        gc2.setTime(dataUltimoLance);
        
        Integer diff = Integer.parseInt(String.valueOf((gc1.getTimeInMillis() - gc2.getTimeInMillis())/1000));
        if (diff < tempoDuracao) {
            res = tempoDuracao - diff;  
        }
        return res;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 41 * hash + (this.leilao != null ? this.leilao.hashCode() : 0);
        hash = 41 * hash + (this.usuario != null ? this.usuario.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Conexao other = (Conexao) obj;
        if (this.leilao != other.getLeilao() && (this.leilao == null || !this.leilao.equals(other.getLeilao()))) {
            return false;
        }
        if (this.usuario != other.getUsuario() && (this.usuario == null || !this.usuario.equals(other.getUsuario()))) {
            return false;
        }
        return true;
    }
    
    private Date inicioJob(Integer segundos) {
        Date dataLimite = null;
        if (segundos != null) {
            GregorianCalendar gc = new GregorianCalendar();
            gc.setTime(new Date());
            gc.add(GregorianCalendar.SECOND, segundos);
            dataLimite = gc.getTime();
        }
        return dataLimite;
    }

    private void criarLanceJob() {
        SchedulerFactory sf = new StdSchedulerFactory();
        try {
            String group = "leilaoGroup" + getLeilao().getId();
            
            JobDetail job = JobBuilder.newJob(LanceJob.class)
                    .withIdentity("lanceJob" + getUsuario().getId(), group)
                    .usingJobData("idLeilao", getLeilao().getId()).build();
            
            SimpleScheduleBuilder schedule = SimpleScheduleBuilder.simpleSchedule();
            
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("lanceTrig" + getUsuario().getId(), group)
                    .withSchedule(schedule).startNow().build();
            
            sched.scheduleJob(job, trigger);
            sched.start();
        } catch (SchedulerException ex) {
            logger.error(ex);
        }
    }

    @Override
    protected void finalize() throws Throwable {
        if (sched != null) {
            sched.shutdown();
            sched = null;
        }
        super.finalize();
    }
    
}
